library(dplyr)
library(ggplot2)
library(ggrepel)
library(knitr)
library(lubridate)
library(plotly)
library(purrr)
library(rrricanes)
library(stringr)
library(tibble)
library(tidyr)

Per the National Weather Service in Houston, TX, the following data are preliminary results from Hurricane Harvey:

THE DATA SHOWN HERE ARE PRELIMINARY….AND SUBJECT TO UPDATES AND CORRECTIONS AS APPROPRIATE.

THIS REPORT INCLUDES EVENTS OCCURRING WHEN WATCHES AND/OR WARNINGS WERE IN EFFECT…OR WHEN SIGNIFICANT FLOODING ASSOCIATED WITH HARVEY OR ITS REMNANTS WAS AFFECTING THE AREA.

COUNTIES INCLUDED…MADISON…MADISON…WALKER…BRAZOS…GRIMES…MONTGOMERY…SAN JACINTO…POLK…WASHINGTON…BURLESON…WALLER…HARRIS…LIBERTY…AUSTIN…COLORADO…WHARTON…FORT BEND…JACKSON…MATAGORDA…BRAZORIA…GALVESTON…CHAMBERS…TRINITY

# Load raw data
rpt <- "https://nwschat.weather.gov/p.php?pid=201709062030-KHGX-ACUS74-PSHHGX"
txt <- readLines(rpt)

# Set base plot
bp <- al_tracking_chart(color = "black", fill = "white", size = 0.1, res = 50)
## Regions defined for each Polygons
## Regions defined for each Polygons

Lowest SLP, Maximum Sustained Winds and Gusts

# subset section A
sec_a <- txt[grep("^A\\. LOWEST SEA LEVEL PRESSURE", txt):(grep("^B\\. MARINE OBSERVATIONS", txt) - 1)]
# trim head
sec_a <- sec_a[-c(1:9)]
# trim tail
sec_a <- head(sec_a, n = -4L)

# subset section B
sec_b <- txt[grep("^B\\. MARINE OBSERVATIONS", txt):(grep("^C\\. STORM TOTAL RAINFALL", txt) - 1)]
# trim head
sec_b <- sec_b[-c(1:8)]
# trim tail
sec_b <- head(sec_b, n = -5L)

# Bind to sec
sec <- c(sec_a, sec_b)

# There is a subheader in middle I'll also manually remove
sec <- sec[-c(78:88)]

# Get station header data
stations <- str_match(sec, 
                      sprintf("^%s[\\s-]*?%s[\\s-]*?%s[\\s]+$", 
                              "([[K|X|J|4]\\w]{3,5})",   # Station
                              "([\\w\\s/\\.\\(\\)]+)",       # Location
                              "([\\D]{2})")) %>%             # State
  .[complete.cases(.),]

# Get station obs. I really wanted to expand the regex to do this; use it as 
# a learning opportunity. But, apparently my skills aren't quite that 
# advanced... oh, well
obs <- str_match(sec, "^(\\d{2}\\.\\d{2}.+$)")[,2] %>% .[complete.cases(.)]

# Move to dataframe
slp <- as_data_frame(
  list("Station" = stations[,2], 
       "Location" = stations[,3], 
       "State" = stations[,4], 
       "Lat" = as.numeric(str_sub(obs, 0L, 5L)), 
       "Lon" = as.numeric(str_sub(obs, 7L, 12L)), 
       "MinPres" = as.numeric(str_sub(obs, 16L, 21L)), 
       "MinPresDt" = as.POSIXct(ymd_hm(sprintf("2017-08-%s %s", 
                                               str_sub(obs, 23L, 24L), 
                                               str_sub(obs, 26L, 29L))), 
                               tz = "UTC"), 
       "Remarks" = str_sub(obs, 31L, 31L), 
       "MaxSustDir" = as.numeric(str_sub(obs, 33L, 35L)), 
       "MaxSustSpd" = as.numeric(str_sub(obs, 37L, 39L)),
       "MaxSustDt" = as.POSIXct(ymd_hm(sprintf("2017-08-%s %s", 
                                               str_sub(obs, 42L, 43L), 
                                               str_sub(obs, 45L, 48L))), 
                               tz = "UTC"), 
       "PeakGustDir" = as.numeric(str_sub(obs, 52L, 54L)), 
       "PeakGustSpd" = as.numeric(str_sub(obs, 56L, 58L)),
       "PeakGustDt" = as.POSIXct(ymd_hm(sprintf("2017-08-%s %s", 
                                                str_sub(obs, 60L, 61L), 
                                                str_sub(obs, 63L, 66L))), 
                               tz = "UTC"))) %>% 
  mutate(Lat = if_else(Lat < 0, Lat * -1, Lat), 
         Lon = if_else(Lon > 0, Lon * -1, Lon))
## Warning: 13 failed to parse.
## Warning: 1 failed to parse.

## Warning: 1 failed to parse.
# Clean up
slp$MinPres[slp$MinPres == 9999.0] <- NA
slp$MaxSustDir[slp$MaxSustDir == 999] <- NA
slp$MaxSustSpd[slp$MaxSustSpd == 999] <- NA
p <- bp + 
  geom_point(data = slp, 
             aes(x = Lon, y = Lat, 
                 text = sprintf("%s\n%s, %s\n%s UTC", 
                                Station, 
                                Location, 
                                State, 
                                MaxSustDt), 
                 size = MaxSustSpd, color = MaxSustSpd)) + 
  coord_equal(xlim = c(min(slp$Lon, na.rm = TRUE), max(slp$Lon, na.rm = TRUE)), 
              ylim = c(min(slp$Lat, na.rm = TRUE), max(slp$Lat, na.rm = TRUE)))
## Warning: Ignoring unknown aesthetics: text
ggplotly(p, tooltip = c("text", "size", "color"))

Storm Total Rainfall

Storm total rainfall from Aug. 25, 12:00 UTC (7:00 AM CDT) through Aug. 31 12:00 UTC (7:00 AM CDT)

rain_obs <- txt[grep("^C\\. STORM TOTAL RAINFALL", txt):(grep("^D\\. INLAND FLOODING", txt) - 1)]

# Drop header and tail data
rain_obs <- rain_obs[-c(1:6)]
rain_obs <- head(rain_obs, -2L)

# Drop empties
rain_obs <- rain_obs[rain_obs != ""]

# Here I'll get observations first
pos <- str_match(rain_obs, sprintf("^%s[\\s]+%s[\\s]?$", 
                                 "([\\d\\.]{5})", 
                                 "([-\\d\\.]{6})"))
# Remove NAs
pos <- pos[complete.cases(pos),]

# Get location details
loc <- str_match(rain_obs, sprintf("^%s[\\s]{3,}%s[\\s]{3,}%s[\\s]{3,}%s[\\s]+%s$", 
                                   "([\\w\\d\\s\\.\\-/]{1,29})", # Location
                                   "([\\w\\s]{1,20})",           # County
                                   "([\\w\\d-=]*)",         # ID
                                   "([\\d\\.]{3,5})",           # Rainfall
                                   "(I*)")) %>%             # Remarks
  .[complete.cases(.),]

# Make df
rain <- as_data_frame(
  list("Location" = loc[,2], 
       "County" = loc[,3], 
       "ID" = loc[,4], 
       "Rainfall" = as.numeric(loc[,5]), 
       "Remarks" = loc[,6],
       "Lat" = as.numeric(pos[,2]), 
       "Lon" = as.numeric(pos[,3]))) %>% 
  mutate(Lat = if_else(Lat < 0, Lat * -1, Lat), 
         Lon = if_else(Lon > 0, Lon * -1, Lon))
p <- bp + 
  geom_point(data = rain, 
             aes(x = Lon, y = Lat, 
                 text = sprintf("%s\n%s, %s", 
                                ID, 
                                Location, 
                                County), 
                 size = Rainfall, color = Rainfall)) + 
  coord_equal(xlim = c(min(rain$Lon, na.rm = TRUE), max(rain$Lon, na.rm = TRUE)), 
              ylim = c(min(rain$Lat, na.rm = TRUE), max(rain$Lat, na.rm = TRUE)))
## Warning: Ignoring unknown aesthetics: text
ggplotly(p, tooltip = c("text", "size", "color"))
## We recommend that you use the dev version of ggplot2 with `ggplotly()`
## Install it with: `devtools::install_github('hadley/ggplot2')`

Maximum Storm Surge and Storm Tide

tide_obs <- txt[grep("^E\\. MAXIMUM STORM SURGE AND STORM TIDE", txt):(grep("^F\\. TORNADOES", txt) - 1)]
# Trim the edges
tide_obs <- tide_obs[-c(1:7)]
tide_obs <- head(tide_obs, n = -5L)
tide_obs[tide_obs == ""] <- NA
tide_obs <- tide_obs[complete.cases(tide_obs)]

tide <- as_data_frame(
  list("County" = str_trim(str_sub(tide_obs, 0L, 16L)), 
       "Location" = str_trim(str_sub(tide_obs, 18L, 32L)), 
       "Surge" = as.numeric(str_trim(str_sub(tide_obs, 34L, 39L))), 
       "Tide" = as.numeric(str_trim(str_sub(tide_obs, 42L, 46L))), 
       "DateTime" = as.POSIXct(ymd_hm(sprintf("2017-08-%s %s", 
                                              str_sub(tide_obs, 49L, 50L), 
                                              str_sub(tide_obs, 52L, 55L))), 
                               tz = "UTC"), 
       "Erosion" = str_trim(str_sub(tide_obs, 58L, 64L))))
kable(tide, caption = "Storm Surge and Storm Tide Totals (ft)")
(#tab:tide_table)Storm Surge and Storm Tide Totals (ft)
County Location Surge Tide DateTime Erosion
HARRIS G MORGANS POINT 3.88 3.53 2017-08-27 19:30:00 UNKNOWN
HARRIS LYNCHBURG LANDI 7.70 7.27 2017-08-29 07:06:00 UNKNOWN
HARRIS MANCHESTER 11.44 10.35 2017-08-29 03:18:00 UNKNOWN
GALVESTON HIGH ISLAND 5.03 4.00 2017-08-31 03:06:00 UNKNOWN
GALVESTON ROLLOVER PASS 3.61 2.99 2017-08-26 18:30:00 UNKNOWN
GALVESTON EAGLE POINT 4.18 3.67 2017-08-29 02:24:00 UNKNOWN
GALVESTON GALVESTON BAY E 2.73 2.58 2017-08-29 03:54:00 UNKNOWN
GALVESTON GALVESTON PIER 3.76 3.57 2017-08-26 02:00:00 UNKNOWN
GALVESTON GALVESTON RAILR 3.14 2.76 2017-08-29 16:06:00 UNKNOWN
BRAZORIA SAN LUIS PASS 3.22 3.32 2017-08-26 00:42:00 UNKNOWN
BRAZORIA FREEPORT 3.19 2.52 2017-08-25 22:00:00 UNKNOWN
MATAGORDA SARGENT 3.19 2.97 2017-08-29 21:18:00 UNKNOWN
MATAGORDA MATAGORDA CITY 3.27 3.12 2017-08-26 08:54:00 UNKNOWN

Tornadoes

tor_obs <- txt[grep("^F\\. TORNADOES", txt):(grep("^G\\. STORM IMPACTS BY COUNTY", txt) - 1)]
tor_obs <- tor_obs[-c(1:6)]
tor_obs <- head(tor_obs, n = -3L)

# county, time and tornado scale
loc <- str_match(tor_obs, "^([\\w\\d\\s]{28})\\s+([\\w\\s]{16})\\s+([\\d/]{7})\\s+(EF\\d)\\s+$") %>% 
  .[complete.cases(.),]

# lat, lon
pos <- str_match(tor_obs, "^([\\d\\.]{4,5})\\s+([-\\d\\.]{4,6})$") %>% 
  .[complete.cases(.),]

# Now, get the remarks. Subtract loc and pos to make it easier.
# Essentially, what I'm doing here is joining the vector together 
# with a \t. Consecutive \t's indicate a break between remarks so 
# make that a newline. Then split on \n and trim, return to vector.
rmks <- tor_obs[!(tor_obs %in% c(loc, pos))]
rmks <- str_c(rmks, collapse = "\t")
rmks <- str_replace_all(rmks, "\t\t+", "\n")
rmks <- str_replace_all(rmks, "\t", " ")
rmks <- str_split(rmks, "\n")
rmks <- map(rmks, str_trim, side = "both") %>% flatten_chr()

tors <- as_data_frame(
  list("Location" = loc[,2], 
       "County" = loc[,3], 
       "Date" = as.POSIXct(ymd_hm(sprintf("2017-08-%s %s", 
                                          str_sub(loc[,4], 0L, 2L), 
                                          str_sub(loc[,4], 4L, 7L)))), 
       "Scale" = factor(loc[,5], 
                        levels = c("EF0", "EF1", "EF2", "EF3", "EF4", "EF5"), 
                        labels = c("EF0", "EF1", "EF2", "EF3", "EF4", "EF5")), 
       "Lat" = as.numeric(pos[,2]), 
       "Lon" = as.numeric(pos[,3]), 
       "Remarks" = rmks))
Scale Wind (mph) Wind (kts)
EFO 65-85 55-74
EF1 86-110 75-96
EF2 111-135 97-117
EF3 136-165 118-143
EF4 166-200 143-174
EF5 >200 >175
tmp <- tors %>% filter(Lat > 0, Lon < 0) # Filter out missing values
p <- bp + 
  geom_point(data = tmp, 
             aes(x = Lon, y = Lat, 
                 text = sprintf("%s", 
                                str_wrap(Remarks)), 
                 size = Scale, color = Scale)) + 
  coord_equal(xlim = c(min(tmp$Lon, na.rm = TRUE), max(tmp$Lon, na.rm = TRUE)), 
              ylim = c(min(tmp$Lat, na.rm = TRUE), max(tmp$Lat, na.rm = TRUE)))
## Warning: Ignoring unknown aesthetics: text
ggplotly(p, tooltip = c("text", "size", "color"))
## We recommend that you use the dev version of ggplot2 with `ggplotly()`
## Install it with: `devtools::install_github('hadley/ggplot2')`
## Warning: Using size for a discrete variable is not advised.
kable(tors, caption = "Tornadoes")
Table 1: Tornadoes
Location County Date Scale Lat Lon Remarks
9 E TIKI ISLAND GALVESTON 2017-08-25 19:22:00 EF0 29.31 -94.77 PUBLIC REPORTS A FUNNEL CLOUD AND A METAL FENCE DAMAGED NEAR FERRY RD IN GALVESTON; TORNADO START TIME 1922 UTC END TIME 1924 CDT.
SARGENT MATAGORDA 2017-08-25 20:45:00 EF0 28.83 -95.66 SHED AND FENCE DAMAGE REPORTED FROM A TORNADO IN SARGENT.
9 E TIKI ISLAND GALVESTON 2017-08-25 20:47:00 EF0 29.31 -94.77 TORNADO DAMAGED MCDONALDS SIGN AT SEAWALL BLVD AND BROADWAY AVE.
2 E ANGLETON BRAZORIA 2017-08-26 04:17:00 EF1 29.17 -95.39 BRAZORIA COUNTY LAW ENFORCEMENT POSSIBLE TORNADO DAMAGE… HOUSE OFF FOUNDATION COUNTY RD. 210 AND FM 523. TIME GIVEN WAS 1117 PM BUT STILL NEED TO VERIFY WITH RADAR, TIME DID NOT MATCH.
4 W ARCOLA FORT BEND 2017-08-26 05:50:00 EF1 29.49 -95.53 HOMES DAMAGE ON VIEUX CARRE MINOR INJURIES AND RESPONDING DEPUTY BLOWN OFF THE ROAD.
6 E FULSHEAR FORT BEND 2017-08-26 07:00:00 EF0 29.70 -95.78 ROOF DAMAGE REPORTED TO A HOME NEAR WESTPARK TOLLWAY AND THE GRAND PARKWAY.
6 E FULSHEAR FORT BEND 2017-08-26 07:00:00 EF0 29.70 -95.78 ROOF DAMAGE REPORT TO A HOME NEAR WESTPARK TOLLWAY AND THE GREEN PARKWAY. THE TIME ESTIMATED FROM RADAR AND BROADCAST MEDIA ESTIMATED AROUND 2AM.
3 S MANVEL BRAZORIA 2017-08-26 07:30:00 EF0 29.44 -95.36 REPORT CAME IN FROM THE MANVEL EOC REGARDING DAMAGED VEHICLES AND BUILDING DAMAGE. POSSIBLY OCCURED BETWEEN 215-245 AM CDT BUT NO COUPLET SEEN ON HGX NOR HOU RADAR DURING THAT TIME.
1 WSW KATY FORT BEND 2017-08-26 10:20:00 EF1 29.79 -95.84 SIGNIFICANT DAMAGE TO A RV…BOAT AND STORAGE FACILITY.
4 SE HUMBLE HARRIS 2017-08-26 12:44:00 EF1 29.95 -95.23 BROADCAST MEDIA REPORTING TORNADO TOUCHDOWN RESULTING IN ROOF…TREE..AND FENCE DAMAGE IN LAKESHORE SUBDIVISION
4 SSW WELLBORN BURLESON 2017-08-26 13:05:00 EF0 30.48 -96.32 TREE DAMAGE REPORTED NEAR OLYMPIA BUDDY ROAD.
2 SW CYPRESS HARRIS 2017-08-26 21:08:00 EF1 29.95 -95.73 TREES BLOWN OVER AND MINOR ROOF DAMAGE REPORTED NEAR BARKER CYPRESS AND TUCKERTON.
2 SE CYPRESS HARRIS 2017-08-26 21:27:00 EF1 29.95 -95.67 ANOTHER TORNADO CONFIRMED ON THE GROUND IN THE VICINITY OF HIGHWAY 290 AND BARKER CYPRESS.
1 NNE EAST BERNARD WHARTON 2017-08-26 22:30:00 EF0 29.54 -96.06 TREES DOWN SE-NW PATH… DAMAGED HOME AND HORSE TRAILER OVERTURNED.
1 SW EAST BERNARD FORT BEND 2017-08-26 22:32:00 EF1 29.52 -96.06 THE TORNADO BEGAN AS A WEAK EF-0 IN DOWNTOWN EAST BERNARD AND THEN TRAVELED NORTHWEST ACROSS ALT HWY 90 WHERE IT STRENGTHENED TO AN EF-1 SNAPPING SEVERAL LARGE MATURE OAK AND PECAN TREES. A HOUSE SUFFERED SIGNIFICANT BRICK FACADE DAMAGE TO ONE SIDE OF THE HOME. SEVERAL TREES WERE DOWNED AND/OR SNAPPED ALONG THE PATH.
2 W IOWA COLONY BRAZORIA 2017-08-26 05:50:00 EF0 0.00 0.00 A STRONG EF-0 TORNADO STRUCK A FAIRLY NEW SUBDIVISION ALONG COUNTY ROAD 56 AND HIGHWAY 288. DAMAGE WAS MOSTLY CONFINED TO ROOFS… FENCES…AND SEVERAL TREES SNAPPED AND/OR DOWNED.
2 W LIVERPOOL BRAZORIA 2017-08-26 04:28:00 EF0 0.00 0.00 AN EF-0 TORNADO TOOK DOWN 4 POWER POLES ON HIGHWAY 35 ALONG WITH SEVERAL TREES NEAR THE GULF COAST SPEEDWAY. THE TORNADO THEN TRAVELED ACROSS GENERALLY OPEN FIELD BEFORE DAMAGING SOME BARNS AND OUTBUILDINGS AS WELL AS TREES ON COUNTY ROAD 511.
1 SSW DANBURY BRAZORIA 2017-08-26 02:44:00 EF1 0.00 0.00 THE TORNADO BEGAN IN DANBURY AND DAMAGED A BARN ALONG WITH SEVERAL TREES OFF OF COUNTY ROAD 207. THE TORNADO THEN CROSSED HIGHWAY 35 AND MOVED OVER AN OPEN FIELD. THE TORNADO THEN SNAPPED AND/OR DOWNED SEVERAL TREES ALONG COUNTY RD 45 BEFORE LIFTING AT THE CROCODILE ENCOUNTER ON COUNTY RD 48.
3 W BAILEYS PRARIE BRAZORIA 2017-08-26 01:25:00 EF0 0.00 0.00 A HIGH-END EF-0 TORNADO TOUCHED DOWN JUST EAST OF WEST COLUMBIA DAMAGING NUMEROUS TREES…ROOFS…AND OUTBUILDINGS IN A NEIGHBORHOOD OFF OF HIGHWAY 35 AND RIVER ROAD. A BARN AND SEVERAL OUTBUILDINGS WERE ALSO DESTROYED ON THE EAST SIDE OF THE BRAZOS RIVER.
1 SSE BRAZORIA BRAZORIA 2017-08-25 20:30:00 EF0 0.00 0.00 A BRIEF TORNADO CROSSED HWY 36 WITH ONE DOWNED POWERLINE AND SEVERAL TREES SNAPPED AND/OR DOWNED.
17 SW JONES CREEK MATAGORDA 2017-08-25 21:14:00 EF1 0.00 0.00 A BRIEF YET STRONG TORNADO MOVED ONSHORE ALONG THE COAST IN SARGENT CAUSING SIGNIFICANT DAMAGE TO ONE HOME AS WELL AS OVERTURNING A MOTORHOME. NUMEROUS TREES WERE SNAPPED AND/OR DOWNED ALONG THE PATH AS WELL AS MINOR ROOF DAMAGE TO SEVERAL HOMES AND BUSINESSES.
1 NW BACLIFF GALVESTON 2017-08-27 10:05:00 EF0 29.50 -94.99 SHORT DAMAGE PATH. TREES DOWN…MINOR DAMGE TO ROOF.

Session Info

pander::pander(sessionInfo())

R version 3.4.0 (2017-04-21)

**Platform:** x86_64-pc-linux-gnu (64-bit)

locale: LC_CTYPE=en_US.UTF-8, LC_NUMERIC=C, LC_TIME=en_US.UTF-8, LC_COLLATE=en_US.UTF-8, LC_MONETARY=en_US.UTF-8, LC_MESSAGES=C, LC_PAPER=en_US.UTF-8, LC_NAME=C, LC_ADDRESS=C, LC_TELEPHONE=C, LC_MEASUREMENT=en_US.UTF-8 and LC_IDENTIFICATION=C

attached base packages: methods, stats, graphics, grDevices, utils, datasets and base

other attached packages: bindrcpp(v.0.2), tidyr(v.0.6.3), tibble(v.1.3.3), stringr(v.1.2.0), rrricanes(v.0.2.0-6), purrr(v.0.2.2.2), plotly(v.4.7.1), lubridate(v.1.6.0), knitr(v.1.16), ggrepel(v.0.6.5), ggplot2(v.2.2.1) and dplyr(v.0.7.2)

loaded via a namespace (and not attached): Rcpp(v.0.12.12), rrricanesdata(v.0.0.1.4), highr(v.0.6), compiler(v.3.4.0), plyr(v.1.8.4), bindr(v.0.1), tools(v.3.4.0), digest(v.0.6.12), lattice(v.0.20-35), viridisLite(v.0.2.0), jsonlite(v.1.5), evaluate(v.0.10.1), gtable(v.0.2.0), pkgconfig(v.2.0.1), rlang(v.0.1.1), shiny(v.1.0.3), crosstalk(v.1.0.0), yaml(v.2.1.14), blogdown(v.0.0.56), rnaturalearthdata(v.0.1.0), httr(v.1.2.1), htmlwidgets(v.0.9), rprojroot(v.1.2), grid(v.3.4.0), glue(v.1.1.1), data.table(v.1.10.4), R6(v.2.2.2), rmarkdown(v.1.6), bookdown(v.0.4), sp(v.1.2-5), pander(v.0.6.0), magrittr(v.1.5), backports(v.1.1.0), scales(v.0.4.1), htmltools(v.0.3.6), assertthat(v.0.2.0), xtable(v.1.8-2), mime(v.0.5), colorspace(v.1.3-2), httpuv(v.1.3.5), labeling(v.0.3), stringi(v.1.1.5), lazyeval(v.0.2.0) and munsell(v.0.4.3)